在寫React的時候,常常會看到有人提到immutable.js,今天要來介紹這套library,了解原理後會覺得它真的是一套很棒的工具喔!介紹它之前要先說明一下javascript的mutable,在javascript中Number、String、Boolean等都是儲存值,但是Object和Array都是儲存reference,reference就是指向的記憶體位置,並不是真正的值,所以當複製Object或Array時,對複製後的值修改也會改到原本的值,這就是所謂的mutable。
以下範例可以直接在console執行看看,就會比較清楚mutable的特性:
const x = { a:1, b:2 };
// y複製的是x的reference
const y = x;
y.a = 10;
console.log(x.a); // 10
console.log(y.a); // 10
由於javascript mutable的特性,在網路上可以找到有滿多人實作deepClone、deepCopy:
function deepClone(obj) {
if (!isObject(obj)) return obj;
const cloneObj = isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (isObject(value)) {
cloneObj[key] = deepClone(value);
} else {
cloneObj[key] = value;
}
}
}
return cloneObj;
}
可以從Demo看到實際執行後,Object跟Array都可以完全被複製值(deep clone),而不是複製reference。而deepClone雖然可以解決mutable的問題,讓我們在做Redux時,不用顧慮是否修改到prev state,但是一旦要更新Object就要整個複製,有點太浪費資源,所以Facebook工程師在2013年時就推出這套ImmutableJS。
Immutable相對於mutable就是指說,一旦data被建立就不可以再改變,但是它又和deep clone不同,因為它只複製有變動的節點父層以上的地方。看下面這張圖會更清楚,當我們要變動F這個節點的資料時,會先copy這個節點,再往上copy它全部的父節點(A、C),然後其他沒有改變的節點依然指向原本的位置,這就是ImmutableJS的實作概念,有效地節省了不必要的浪費。
Immutable API提供了許多 Persistent Immutable data structures ,像是List、Stack、Map、OrderedMap、Set、OrderedSet還有Record,當我們在操作這些immutable data時,要記得它們 always yield new updated data ,透過API操作的時候,回傳的都會是新的data,所以要再指定回去。
Immutable如剛剛提到的提供許多不同的資料型態,這邊我只簡單介紹幾個比較常用到的,有興趣看更多的人可以參考Immutable Doc。
要使用之前,我們需要先安裝immutable package:
npm install immutable --save
Deeply converts plain JS objects and arrays to Immutable Maps and Lists.
我們在javascript中使用的Object跟Array,可以透過fromJS()
幫我們把這些javascript mutable的資料型態轉換成immutable,Object轉換後會變成Map,Array轉換後會變成List,而且fromJS()
會把資料內的每一層都做轉換。
使用範例:
const mutableArray = [1, 2, 3];
const immutableArray = Immutable.fromJS(mutableArray);
const mutableObject = { x: 1, y: 2 };
const immutableObject = Immutable.fromJS(mutableObject);
Immutable Map可以從javascript Object轉換而來,也可以把它想成就是Object的替代。
set(key, value)
改變值,並且回傳一個新的Map。get(key)
取值。以下直接用範例說明:
// 使用fromJS轉換成Immutable Map
const map1 = Immutable.fromJS({a: 10, b: 20, c: 30});
const map2 = map1.set('a', 100);
console.log(map1.get('a')); // 10
console.log(map2.get('a')); // 100
setIn(keyPath, value)
改變值,並且回傳一個新的Map。get(keyPath)
取值。// 使用fromJS轉換成Immutable Map
const map1 = Immutable.fromJS({
name: 'Jack',
profile: {
age: 30,
height: 170,
weight: 70
}
});
const map2 = map1.setIn(['profile', 'age'], 40);
console.log(map1.getIn(['profile', 'age'])); // 30
console.log(map2.getIn(['profile', 'age'])); // 40
Immutable List可以從javascript Array轉換而來,也可以把它想成就是Array的替代。
set(index)
改變值,並且回傳一個新的Map。get(index)
取值。const list1= Immutable.fromJS(['a', 'b', 'c']);
const list2 = list1.set(1, 'z');
console.log(list1.get(1)); // 'b'
console.log(list2.get(1)); // 'z'
setIn(keyPath, value)
改變值,並且回傳一個新的Map。get(keyPath)
取值。const list1= Immutable.fromJS([ 'a', 'b', ['x', 'y', 'z'] ]);
const list2 = list1.setIn([2, 1], 'GJ');
console.log(list1.getIn([2, 1])); // 'y'
console.log(list2.getIn([2, 1])); // 'GJ'
List也有提供其他像Array對應的方法可以使用,EX:pop
、push
、shift
...等,使用概念都相同,只有需要特別注意的是每次有修改裡面的值,不管是第一層還是deep層,都會回傳一個新的data回來,就像上面set
和SetIn
那樣,因為我們現在的data已經是immutable囉!
當我們在寫Redux時,可以透過使用ImmutableJS的幫助來操作data,這樣比較能確保prev state不會被修改到,但是如果我們把immutable的data log出來,可能會發現這樣的data很難看懂,這時候可以使用Map和List都有的toJS()
,來把immutable切換回javascript的格式。
以下加上redux-logger來說明:
import { createStore, applyMiddleware } from 'redux';
import { Map } from 'immutable';
import createLogger from 'redux-logger';
import rootReducer from './reducers';
// 建立Map,指定給initialState
const initialState = Map({});
// 建立store時,把initialState也指定進去
const store = createStore(rootReducer, initialState, applyMiddleware(
createLogger({
stateTransformer: state => state.toJS() // log前先轉換state.toJS()
})
));
export default store;
在寫Redux的時候,會發現操作多層的Object是一件很辛苦的事情,尤其是複雜又多層的json,使用ImmutableJS是比deep copy更好的解法,另外,其實也可以參考normalizr
,它的意思就是把JSON格式扁平化,避免深層的JSON,我覺得是用另一種方式來處理這類問題,有興趣也可以看看它的概念再決定要用哪種方式來處理較複雜的Redux state格式問題噢!